package com.szeldon.declatest.eval;

import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Stack;

import com.szeldon.declatest.eval.exceptions.EvaluationException;
import com.szeldon.declatest.exec.AbstractTestsSuiteExecutor;
import com.szeldon.declatest.exec.TestSuiteExecHistory;

/**
 * Evaluator for parts of scenario in Declatest. Extensible by using EvaluatorPlugin objects.
 * 
 * @author szeldon
 * 
 */
public final class ScenarioPartEvaluator {

    /** Separator for evaluator's plugin separator and expression compatible with that plugin. */
    public final static char PREFIX_SEPARATOR = '#';

    /** Empty string being a prefix for standard library plugin. */
    public final static String STANDARD_LIBRARY_PREFIX = "";

    /** Start tag of an expression. */
    public static final String EXPRESSION_START = "${";

    /** End tag of an expression. */
    public static final String EXPRESSION_END = "}";

    /** Map of plugins as value and plugin prefix as key. */
    private Map<String, IScenarioEvaluatorPlugin> pluginsMap = new HashMap<String, IScenarioEvaluatorPlugin>();

    /**
     * Evaluates condition with given parameters.
     * 
     * @param properties
     * 
     * @param executor
     * 
     * @param left
     *            Left value.
     * @param operator
     *            Operator to be used for comparison between values.
     * @param right
     *            Right value.
     * @param executor
     *            Executor.
     * @param history
     * @return
     * @throws EvaluationException
     *             Thrown when there was a problem during evaluation.
     */
    public ConditionReturnValue evaluateCondition(Properties properties, AbstractTestsSuiteExecutor executor,
            String left, String operator, String right, TestSuiteExecHistory history) throws EvaluationException {

        String leftValue = evaluateValue(left, executor, properties, history);
        String rightValue = evaluateValue(right, executor, properties, history);

        String[] operatorSplit = extractPluginNameAndExpression(operator);
        IScenarioEvaluatorPlugin operatorPlugin = pluginsMap.get(operatorSplit[0]);

        boolean evaluatedTrue = operatorPlugin.evaluateOperator(executor, leftValue, operatorSplit[1], rightValue,
                properties, history);

        return new ConditionReturnValue(evaluatedTrue, leftValue, rightValue);
    }

    /**
     * Evaluates given value. If there are expressions inside, they're evaluated using plugins. Expressions may be
     * nested. Returned value depends on plugins.
     * 
     * @param value
     * @param executor
     * @param properties
     * @param history
     * @return
     * @throws EvaluationException
     *             Thrown when there was a problem during evaluation.
     */
    private String evaluateValue(String value, AbstractTestsSuiteExecutor executor, Properties properties,
            TestSuiteExecHistory history) throws EvaluationException {
        StringBuilder valueBuilder = new StringBuilder(value);

        // Stack that will store indexes where expressions (${expression}) start. These are indexes of a first character
        // of an expression.
        Stack<Integer> expressionStartIndexStack = new Stack<Integer>();

        for (int i = 0; i < valueBuilder.length(); i++) {
            char currentChar = valueBuilder.charAt(i);
            // pushes an index, if start tag is the current character of value
            if (currentChar == EXPRESSION_START.charAt(0)) {
                i++;
                if (i == valueBuilder.length()) {
                    throw new EvaluationException("Corrupted value to be evaluated. Can't use '$' without '{'. value="
                            + value);
                }
                if (valueBuilder.charAt(i) == EXPRESSION_START.charAt(1)) {
                    expressionStartIndexStack.push(i + 1);
                }
            } // pops an index, if current character is a closing tag
            else if (currentChar == EXPRESSION_END.charAt(0)) {
                if (expressionStartIndexStack.empty()) {
                    throw new EvaluationException(
                            "Corrupted value to be evaluated. Can't have a '}' without preceeding '${'. value=" + value);
                }

                // gets an expression being between [stack_top, i]
                int expressionStartIndex = expressionStartIndexStack.pop();
                String expression = valueBuilder.substring(expressionStartIndex, i);

                // gets plugin name and expression
                String[] expressionSplit = extractPluginNameAndExpression(expression);
                IScenarioEvaluatorPlugin plugin = pluginsMap.get(expressionSplit[0]);

                if (plugin == null) {
                    throw new EvaluationException("There's no plugin for expression: " + expression + ".");
                }

                // evaluates expression using plugin
                String evaluated = plugin.evaluateValue(executor, expressionSplit[1], properties, history);

                // replaces expression with value evaluated out of it
                valueBuilder.replace(expressionStartIndex - EXPRESSION_START.length(), i + 1, evaluated);

                // changes i so that it stands at last character of string which replaced the expression
                i = i + evaluated.length() - (i - expressionStartIndex) - EXPRESSION_START.length() - 1;

            }
        }

        return valueBuilder.toString();
    }

    /**
     * Adds plugin to this evaluator.
     * 
     * @param plugin
     * @throws IllegalArgumentException
     *             Thrown when given plugin returns prefix ( {@link IScenarioEvaluatorPlugin#getPluginPrefix() ) that
     *             was already added to evaluator.
     */
    public void addPlugin(IScenarioEvaluatorPlugin plugin) throws IllegalArgumentException {
        String pluginPrefix = plugin.getPluginPrefix();
        if (pluginsMap.containsKey(pluginPrefix)) {
            throw new IllegalArgumentException("Plugin with " + pluginPrefix
                    + " prefix was already added to evaluator.");
        }
        if (pluginPrefix.contains("" + ScenarioPartEvaluator.PREFIX_SEPARATOR)) {
            throw new IllegalArgumentException("Plugin name can't contain " + ScenarioPartEvaluator.PREFIX_SEPARATOR
                    + ". pluginName=" + pluginPrefix);
        }

        pluginsMap.put(pluginPrefix, plugin);

    }

    /**
     * Extracts plugin name and expression from given string. Given string is assumed to look basing on this scheme:
     * </br></br> optional_plugin_name#plugin_compatible_expression</br></br> If there's no optional_plugin_name,
     * {@link #STANDARD_LIBRARY_PREFIX} is assumed. Plugin name can't contain "#" because it's a separator between
     * plugin name and expression.
     * 
     * @param value
     * @return Two-element array with plugin name as first element and expression as second one.
     */
    public static String[] extractPluginNameAndExpression(String value) {
        String pluginName = null;
        String expression = null;
        int valuesPrefixEnd = value.indexOf(PREFIX_SEPARATOR);
        if (valuesPrefixEnd > 0) {
            pluginName = value.substring(0, valuesPrefixEnd);
            expression = value.substring(valuesPrefixEnd + 1);
        } else {
            pluginName = STANDARD_LIBRARY_PREFIX;
            expression = value;
        }

        return new String[] { pluginName, expression };

    }
}
